﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Kyoh.Lib.Win32API.Shell;
using Kyoh.Lib.Win32API;
using System.IO;
using System.Threading;

namespace Kyoh.Lib.WinForms
{
    /// <summary>ファイル・フォルダの構造をツリー上に表示する <see cref="TreeView"/> の拡張。</summary>
    public partial class FileSystemTreeView : TreeView
    {
        private IContainer components;
        private ImageList imageListIcons;
        private BackgroundWorker backgroundBuilder;
        private AutoResetEvent buildWait;
        private IconManager iconManager;
        private System.Windows.Forms.Timer buildTimer;
        private FileSystemTreeNode _desktop;
        private FileSystemTreeNode _drives;
        private FileSystemTreeNode _documents;

        /// <summary><see cref="FileSystemTreeView"/> の新しいインスタンスを初期化します。</summary>
        public FileSystemTreeView()
        {
            components = new Container();
            backgroundBuilder = new BackgroundWorker();
            imageListIcons = new ImageList(components)
                                 {
                                     ColorDepth = ColorDepth.Depth32Bit,
                                     ImageSize = new Size(16, 16),
                                     TransparentColor = Color.Transparent,
                                 };
            buildWait = new AutoResetEvent(false);
            buildTimer = new System.Windows.Forms.Timer()
                             {
                                 Interval = 10000,
                                 Enabled = false,
                             };
            buildTimer.Tick += new EventHandler(BuildRun);
            backgroundBuilder.DoWork += new DoWorkEventHandler(Build);
            backgroundBuilder.RunWorkerAsync();
            iconManager = new IconManager(components);
            Initialized = false;
        }

        /// <summary>使用されているアンマネージ リソースを解放し、オプションでマネージ リソースも解放します。</summary>
        /// <param name="disposing">マネージ リソースとアンマネージ リソースの両方を解放する場合は true。アンマネージ リソースだけを解放する場合は false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void BuildRun(object sender, EventArgs e)
        {
            if (BackgroundBuild)
                buildWait.Set();
        }

        private void Build(object sender, DoWorkEventArgs e)
        {
            while (true)
            {
                buildWait.WaitOne();
                if (BackgroundBuild)
                {
                    foreach (var node in Nodes)
                        CreateChildren(node as FileSystemTreeNode);
                }
            }
        }

        private delegate void Action();
        private delegate TRet Func<T, TRet>(T item1);

        /// <summary>ツリービューを初期化します。</summary>
        public void InitializeTree()
        {
            ClearNodes(Nodes);
            ClearIcons();
            AddIcon(new Bitmap(16, 16));
            _iconIndices.Clear();

            var rootNode = CreateRoot();
            AddNode(Nodes, rootNode);
            SetIcon();
            ExpandNode(rootNode);
            ExpandNode(rootNode.Nodes[0]);
            buildTimer.Start();
            Initialized = true;
        }

        /// <summary>初期化が完了しているかどうかを取得します。</summary>
        [Browsable(false)]
        public bool Initialized { get; private set; }

        /// <summary>バックグラウンドで、ツリーの構築を行うかどうかを取得・設定します。</summary>
        [DefaultValue(false)]
        public bool BackgroundBuild { get; set; }

        private readonly Dictionary<IntPtr, int> _iconIndices = new Dictionary<IntPtr, int>();

        private void ExpandNode(TreeNode node)
        {
            if (InvokeRequired)
                Invoke(new Action(node.Expand));
            else
                node.Expand();
        }

        /// <summary>指定されたパスのノードを選択します。</summary>
        /// <param name="path">選択するノードのパス。</param>
        public void SelectPath(string path)
        {
            if (!Path.IsPathRooted(path) ||
                !Directory.Exists(path))
                return;

            var names = new Queue<string>(path.Split(Path.DirectorySeparatorChar));

            lock (selectEventLocker)
                selecting = true;
            SelectPath(_drives, names);
            lock (selectEventLocker)
                selecting = false;
        }

        private bool selecting;
        private object selectEventLocker = new object();

        /// <summary><see cref="System.Windows.Forms.TreeView.AfterSelect"/> イベントを発生させます。</summary>
        protected override void OnAfterSelect(TreeViewEventArgs e)
        {
            lock (selectEventLocker)
                if (selecting)
                    return;
            base.OnAfterSelect(e);
        }
        private void SelectPath(FileSystemTreeNode node, Queue<string> names)
        {
            if (node == null)
                return;

            if (names.Count < 1)
            {
                SelectedNode = node;
                return;
            }

            if(node.HasDummy)
                node.Expand();

            var name = names.Dequeue();
            foreach (TreeNode n in node.Nodes)
            {
                var child = n as FileSystemTreeNode;
                if (child == null)
                    continue;
                if (child.Name == name)
                {
                    //SelectedNode = child;
                    SelectPath(child, names);
                    break;
                }
            }
        }

        private void SetIcon()
        {
            if (InvokeRequired)
                Invoke(new Action(() => ImageList = imageListIcons));
            else
                ImageList = imageListIcons;
        }

        private void AddNode(TreeNodeCollection nodes, TreeNode node)
        {
            if (InvokeRequired)
                Invoke(new Func<TreeNode, int>(nodes.Add), node);
            else
                nodes.Add(node);
        }

        private void AddIcon(Icon icon)
        {
            if (icon != null)
                AddIcon(icon.ToBitmap());
        }

        private void AddIcon(Image icon)
        {
            if (InvokeRequired)
                Invoke(new Action<Image>(imageListIcons.Images.Add), icon);
            else
                imageListIcons.Images.Add(icon);
        }

        private void ClearNodes(TreeNodeCollection nodes)
        {
            if (InvokeRequired)
                Invoke(new Action(nodes.Clear));
            else
                nodes.Clear();
        }

        private void ClearIcons()
        {
            if (InvokeRequired)
                Invoke(new Action(imageListIcons.Images.Clear));
            else
                imageListIcons.Images.Clear();
        }

        private void RemoveByKey(TreeNodeCollection nodes, string key)
        {
            if (InvokeRequired)
                Invoke(new Action<string>(nodes.RemoveByKey), key);
            else
                nodes.RemoveByKey(key);
        }

        /// <summary><see cref="System.Windows.Forms.TreeView.BeforeExpand"/> イベントを発生させます。</summary>
        protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
        {
            Debug.WriteLine("OnBeforeExpand");
            CreateChildren(e.Node as FileSystemTreeNode);
            base.OnBeforeExpand(e);
        }

        private void CreateChildren(FileSystemTreeNode node)
        {
            Debug.WriteLine("CreateChildren: (" + node.Text + ")");

            if (node == null)
                return;
            if (node.HasDummy)
            {
                RemoveByKey(node.Nodes, FileSystemTreeNode.DummyNodeKey);
                try
                {
                    foreach (var child in Directory.GetDirectories(node.Path))
                        AddNode(node.Nodes, CreateEntryNode(child, 0));
                }
                catch
                {
                }
                node.HasDummy = false;
            }

            // 展開されているノードについては再帰的に処理する。
            // （ノードの展開時は、BeforeExpandによるため、対象ノードは展開されていない）
            bool isExpanded;
            if (InvokeRequired)
                isExpanded = (bool)Invoke(new Func<FileSystemTreeNode, bool>(IsExpanded), node);
            else
                isExpanded = IsExpanded(node);

            if (isExpanded)
                foreach (var child in node.Nodes)
                    CreateChildren(child as FileSystemTreeNode);
        }

        private bool IsExpanded(TreeNode node)
        {
            return node.IsExpanded;
        }

        private TreeNode CreateRoot()
        {
            var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            _desktop = CreateEntryNode(desktop, ShellSpecialFolderConstants.DESKTOP);

            // マイコンピュータ
            _drives = CreateEntryNode(null, ShellSpecialFolderConstants.DRIVES);
            foreach (var logicalDrive in Environment.GetLogicalDrives())
                _drives.Nodes.Add(CreateEntryNode(logicalDrive, 0));
            _desktop.Nodes.Add(_drives);

            // マイドキュメント
            var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            _documents = CreateEntryNode(path, ShellSpecialFolderConstants.PERSONAL);
            _desktop.Nodes.Add(_documents);

            _desktop.Nodes.Add(CreateEntryNode(null, ShellSpecialFolderConstants.FAVORITES));
            return _desktop;
        }

        private FileSystemTreeNode CreateEntryNode(string path, ShellSpecialFolderConstants folder)
        {
            string displayName, typeName;
            IntPtr idList;
            IntPtr icon, openIcon;
            if (folder != 0)
                GetFileInfo(folder, out displayName, out typeName, out icon, out openIcon, out idList);
            else if(path != null)
                GetFileInfo(path, out displayName, out typeName, out icon, out openIcon);
            else
                throw new ArgumentNullException("path");

            if (path == null)
            {
                var pathBuilder = new StringBuilder();
                NativeMethods.SHGetFolderPath(IntPtr.Zero, (int)folder, IntPtr.Zero, 0, pathBuilder);
                path = pathBuilder.ToString();
            }

            return new FileSystemTreeNode(displayName,
                                          GetIconIndexFromHandle(icon),
                                          GetIconIndexFromHandle(openIcon),
                                          path);
        }

        private int GetIconIndexFromHandle(IntPtr handle)
        {
            try
            {
                if (_iconIndices.ContainsKey(handle))
                    return _iconIndices[handle];
                else
                {
                    var icon = iconManager.GetIcon(handle);
                    var index = imageListIcons.Images.Count;
                    _iconIndices.Add(handle, index);
                    AddIcon(icon);
                    return index;
                }
            }
            catch
            {
                return 0;
            }
            finally
            {
                if (handle != IntPtr.Zero)
                    NativeMethods.DestroyIcon(handle);
            }
        }

        private static void GetFileInfo(ShellSpecialFolderConstants folder, out string displayName, out string typeName, out IntPtr icon, out IntPtr openIcon, out IntPtr idList)
        {
            NativeMethods.SHGetSpecialFolderLocation(IntPtr.Zero,
                                                     folder,
                                                     out idList);
            var info = new ShFileInfoW();
            NativeMethods.ShGetFileInfo(idList, 0, ref info, (uint)Marshal.SizeOf(info), ShellFileInfoFlags.Pidl | CreateShellFileInfoFlags(false));
            displayName = info.szDisplayName;
            typeName = info.szTypeName;
            icon = info.hIcon;
            NativeMethods.ShGetFileInfo(idList, 0, ref info, (uint)Marshal.SizeOf(info), ShellFileInfoFlags.Pidl | CreateShellFileInfoFlags(true));
            openIcon = info.hIcon;
        }

        private static void GetFileInfo(string path, out string displayName, out string typeName, out IntPtr icon, out IntPtr openIcon)
        {
            var info = new ShFileInfoW();
            NativeMethods.ShGetFileInfo(path, 0, ref info, (uint)Marshal.SizeOf(info), CreateShellFileInfoFlags(false));
            displayName = info.szDisplayName;
            typeName = info.szTypeName;
            icon = info.hIcon;
            NativeMethods.ShGetFileInfo(path, 0, ref info, (uint)Marshal.SizeOf(info), CreateShellFileInfoFlags(true));
            openIcon = info.hIcon;
        }

        private static ShellFileInfoFlags CreateShellFileInfoFlags(bool selected)
        {
            return
                ShellFileInfoFlags.SmallIcon | ShellFileInfoFlags.Icon |
                (selected ? ShellFileInfoFlags.OpenIcon : (ShellFileInfoFlags.DisplayName | ShellFileInfoFlags.TypeName));
        }

        /// <summary>現在選択されているパスを取得します。</summary>
        public string GetSelectedPath()
        {
            var node = SelectedNode as FileSystemTreeNode;
            if (node == null)
                return null;
            return node.Path;
        }
    }
}
